package sample.note;

import android.Manifest;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.WindowManager;

import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;

import lombok.SneakyThrows;

import static sample.note.AppWrapper.cheers;

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    private static CrashHandler INSTANCE;
    private Context mContext;
    String pid;
    File logFileFolder;
    FileOutputStream logOutput;
    String logCmd;
    boolean tracking = true;
    int LENGTH = 1024;
    static Semaphore trackingSemaphore = new Semaphore(0);
    DateFormat dateFmt = new SimpleDateFormat("yyyy-MM-dd_kk_mm_ss");
    private Handler mHandler;


    private CrashHandler() {
    }

    public static CrashHandler getInstance() {
        if (INSTANCE == null)
            INSTANCE = new CrashHandler();
        return INSTANCE;
    }

    @SneakyThrows
    public void boot(Context context) {
        mContext = context;

        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
        mHandler = new Handler(Looper.getMainLooper());

        //trackLogcat(context);
    }

    public void shutdown(){
        new Thread(new Runnable() {
            @SneakyThrows
            public void run() {
                tracking(false);
            }
        }).start();
    }
    boolean tracking() throws InterruptedException {
        boolean tmp = false;
        trackingSemaphore.acquire();
        tmp = tracking;
        trackingSemaphore.release();
        return tmp;
    }
    void tracking(boolean b) throws InterruptedException {
        trackingSemaphore.acquire();
        tracking = b;
        trackingSemaphore.release();
    }

    void trackLogcat(Context context){
        pid = String.valueOf(android.os.Process.myPid());

        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            //logFileFolder = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator ;
            logFileFolder = context.getExternalFilesDir(File.separator);
        } else {
            //logFileFolder = context.getFilesDir().getAbsolutePath() + File.separator;
            logFileFolder = context.getFilesDir();
        }
        logCmd = "logcat *:e *:i *:d *:w *:v *:d | grep \"(" + pid + ")\"";
        Log.i("logcatCmd", logCmd);

        final Thread task = new Thread(new Runnable() {
            @SneakyThrows
            public void run() {
                //if(!storageAvailable()){return;}
                Process logcatProc = null;
                BufferedReader logcatReader = null;

                try{
                    File logFile = new File(logFileFolder, dateFmt.format(new Date()) + ".log");
                    Log.i("logcat_trace", logFile.getAbsolutePath());
                    if(logFile.exists())
                        logFile.delete();
                    logOutput = new FileOutputStream(logFile);
                    logcatProc = Runtime.getRuntime().exec(logCmd);
                    logcatReader = new BufferedReader(new InputStreamReader(logcatProc.getInputStream()), LENGTH);
                    String line = null;

                    while(tracking() && (line = logcatReader.readLine()) !=null ){
                        trackingSemaphore.acquire();
                        if(!tracking)
                            break;
                        if(line.length() == 0)
                            continue;
                        if (logOutput != null && line.contains(pid)) {
                            logOutput.write((line + "\n").getBytes());
                        }
                    }
                }
                finally {
                    if(logcatProc!=null) logcatProc.destroy();
                    if(logcatReader !=null) logcatReader.close();
                    if(logOutput !=null) logOutput.close();
                }
            }
        });
        mHandler.postDelayed(new Runnable() {
            public void run() {
                task.start();
            }
        }, 1024);
    }
    boolean storageAvailable(){
        if(ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            Log.e("storage_available", "false");
            return false;
        }
        Log.e("storage_available", "true");
        return true;
    }

    public void uncaughtException(Thread thread, Throwable ex) {
        //if(!storageAvailable()){return;}
        try {
            //if (!handleException(ex) && mDefaultHandler != null) { mDefaultHandler.uncaughtException(thread, ex); }
            handleException(ex);
            mDefaultHandler.uncaughtException(thread, ex);
        } catch (PackageManager.NameNotFoundException e) {
            Log.e("Reading package info", e.getMessage());
        }
    }

    public boolean handleException(Throwable ex) throws PackageManager.NameNotFoundException {
        if (ex == null || mContext == null)
            return false;
        final String crashReport = crashReport(mContext, ex);
        Log.i("error", crashReport);
        new Thread() {
            @SneakyThrows
            public void run() {
                Looper.prepare();
                File file = null;
                file = str2file(crashReport);
                //if(file !=null){ email(mContext, crashReport, file); }
                Looper.loop();
            }

        }.start();
        return true;
    }

    private File str2file(String crashReport) throws IOException {
        String fileName = CRASH_FILE_PREFIX + dateFmt.format(new Date()) + ".txt";
        if (Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) {
            //File dir = new File(Environment.getExternalStorageDirectory() .getAbsolutePath() + File.separator);
            File dir = mContext.getExternalFilesDir(File.separator);
            if (!dir.exists())
                dir.mkdir();
            File file = new File(dir, fileName);
            Log.i("crash_report", file.getAbsolutePath());
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(crashReport.toString().getBytes());
            fos.close();
            return file;
        }
        return null;
    }

    final String CRASH_FILE_PREFIX = "crash-";
    public class CrashReportFilter implements FileFilter {
        public boolean accept(File file) {
            if(file.isDirectory())
                return true;
            else
            {
                String name = file.getName();
                if(name.startsWith(CRASH_FILE_PREFIX) && name.endsWith(".txt"))
                    return true;
                else
                    return false;
            }

        }
    }

    final CrashReportFilter crashReportFilter = new CrashReportFilter();
    File newestCrashReport(){
        File[] files= mContext.getExternalFilesDir(File.separator).listFiles(crashReportFilter);
        Map<Long, File> map = new HashMap<>();
        for (File f: files) {
            map.put(f.lastModified(), f);
        }
        Object[] obj = map.keySet().toArray();
        if(obj.length == 0){
            return null;
        }
        Arrays.sort(obj);
        return map.get(obj[obj.length - 1]);
    }

    public void email(){
        File file = newestCrashReport();
        if(file == null){
            cheers("No crash report found.");
            return;
        }
        String s = file2str(file);
        email(mContext, s);
    }
    public static String file2str(File f)
    {
        String readOutStr = null;

        try {
            BufferedReader bufReader = new BufferedReader(new FileReader(f));
            String line = "";
            while( ( line = bufReader.readLine() ) != null)
            {
                readOutStr += line;
            }
            bufReader.close();

        } catch (IOException e) {
            readOutStr = null;
        }

        return readOutStr;
    }

    void email(final Context context, final String crashReport){

        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        String EMAIL_ADDRESS = null;
        try {
            EMAIL_ADDRESS = Property.get("email.address");
        } catch (IOException e) {
            Log.e("email address", e.getMessage());
        }
        String[] tos = { EMAIL_ADDRESS };
        intent.putExtra(Intent.EXTRA_EMAIL, tos);

        intent.putExtra(Intent.EXTRA_SUBJECT, "crash report");
        intent.putExtra(Intent.EXTRA_TEXT, "Please send this report to us, We will fix it as soon as possible.\n"+ crashReport);
        intent.setType("text/plain");
        intent.setType("message/rfc882");
        Intent.createChooser(intent, "Choose Email Client");
        context.startActivity(intent);
    }
    void email(final Context context, final String crashReport, final File file){

        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        String EMAIL_ADDRESS = null;
        try {
            EMAIL_ADDRESS = Property.get("email.address");
        } catch (IOException e) {
            Log.e("email address", e.getMessage());
        }
        String[] tos = { EMAIL_ADDRESS };
        intent.putExtra(Intent.EXTRA_EMAIL, tos);

        intent.putExtra(Intent.EXTRA_SUBJECT, "crash report");
        if (file != null) {
            intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
            intent.putExtra(Intent.EXTRA_TEXT, "Please send this report to us, We will fix it as soon as possible.\n");
        } else {
            intent.putExtra(Intent.EXTRA_TEXT, "Please send this report to us, We will fix it as soon as possible.\n"+ crashReport);
        }
        intent.setType("text/plain");
        intent.setType("message/rfc882");
        Intent.createChooser(intent, "Choose Email Client");
        context.startActivity(intent);
    }

    void email_confirm(final Context context, final String crashReport, final File file) {
        AlertDialog mDialog;
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setIcon(android.R.drawable.ic_dialog_info);
        builder.setTitle("error");
        builder.setMessage("Send crash report email");
        builder.setPositiveButton(android.R.string.ok,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {

                        email(context, crashReport, file);
                        dialog.dismiss();
                        android.os.Process.killProcess(android.os.Process .myPid());
                        System.exit(1);
                    }
                });
        builder.setNegativeButton(android.R.string.cancel,
                new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                        android.os.Process.killProcess(android.os.Process .myPid());
                        System.exit(1);
                    }
                });
        mDialog = builder.create();
        mDialog.getWindow().setType( WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
        mDialog.show();
    }
    private String crashReport(Context context, Throwable ex) throws PackageManager.NameNotFoundException {
        PackageInfo pinfo = packInfo(context);
        StringBuffer exceptionStr = new StringBuffer();
        exceptionStr.append("Version: " + pinfo.versionName + "(" + pinfo.versionCode + ")\n");
        exceptionStr.append("Android: " + android.os.Build.VERSION.RELEASE + "(" + android.os.Build.MODEL + ")\n");
        exceptionStr.append("Exception: " + ex.getMessage() + "\n");
        StackTraceElement[] elements = ex.getStackTrace();
        for (int i = 0; i < elements.length; i++) {
            exceptionStr.append(elements[i].toString() + "\n");
        }
        return exceptionStr.toString();
    }

    private PackageInfo packInfo(Context context) throws PackageManager.NameNotFoundException {
        PackageInfo info = null;
        info = context.getPackageManager().getPackageInfo( context.getPackageName(), 0);
        if (info == null) info = new PackageInfo();
        return info;
    }

}
